/*

 * Licensed to the Apache Software Foundation (ASF) under one

 * or more contributor license agreements.  See the NOTICE file

 * distributed with this work for additional information

 * regarding copyright ownership.  The ASF licenses this file

 * to you under the Apache License, Version 2.0 (the

 * "License"); you may not use this file except in compliance

 * with the License.  You may obtain a copy of the License at

 *

 *     http://www.apache.org/licenses/LICENSE-2.0

 *

 * Unless required by applicable law or agreed to in writing, software

 * distributed under the License is distributed on an "AS IS" BASIS,

 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

 * See the License for the specific language governing permissions and

 * limitations under the License.

 */

package com.bff.gaia.unified.sdk.io.gcp.spanner;



import com.bff.gaia.unified.vendor.guava.com.google.common.annotations.VisibleForTesting;

import com.bff.gaia.unified.vendor.guava.com.google.common.math.LongMath;

import com.bff.gaia.unified.vendor.guava.com.google.common.primitives.Longs;

import com.bff.gaia.unified.vendor.guava.com.google.common.primitives.UnsignedInteger;



import java.math.RoundingMode;

import java.util.ArrayList;

import java.util.Arrays;



import static com.bff.gaia.unified.vendor.guava.com.google.common.base.Preconditions.checkArgument;



/**

 * This module provides routines for encoding a sequence of typed entities into a byte array. The

 * resulting byte arrays can be lexicographically compared to yield the same comparison value that

 * would have been generated if the encoded items had been compared one by one according to their

 * type.

 *

 * <p>More precisely, suppose: 1. byte array A is generated by encoding the sequence of items

 * [A_1..A_n] 2. byte array B is generated by encoding the sequence of items [B_1..B_n] 3. The types

 * match; i.e., for all i: A_i was encoded using the same routine as B_i

 *

 * <p>Then: Comparing A vs. B lexicographically is the same as comparing the vectors [A_1..A_n] and

 * [B_1..B_n] lexicographically.

 *

 * <p><b>This class is NOT thread safe.</b>

 */

class OrderedCode {

  // We want to encode a few extra symbols in strings:

  //      <sep>           Separator between items

  //      <infinity>      Infinite string

  //

  // Therefore we need an alphabet with at least 258 characters.  We

  // achieve this by using two-letter sequences starting with '\0' and '\xff'

  // as extra symbols:

  //      <sep>           encoded as =>           \0\1

  //      \0              encoded as =>           \0\xff

  //      \xff            encoded as =>           \xff\x00

  //      <infinity>      encoded as =>           \xff\xff

  //

  // The remaining two letter sequences starting with '\0' and '\xff'

  // are currently unused.



  static final byte ESCAPE1 = 0x00;

  static final byte NULL_CHARACTER = (byte) 0xff; // Combined with ESCAPE1

  static final byte SEPARATOR = 0x01; // Combined with ESCAPE1



  static final byte ESCAPE2 = (byte) 0xff;

  static final byte INFINITY = (byte) 0xff; // Combined with ESCAPE2

  static final byte FF_CHARACTER = 0x00; // Combined with ESCAPE2



  static final byte[] ESCAPE1_SEPARATOR = {ESCAPE1, SEPARATOR};



  static final byte[] INFINITY_ENCODED = {ESCAPE2, INFINITY};



  static final byte[] INFINITY_ENCODED_DECREASING = {invert(ESCAPE2), invert(INFINITY)};



  /**

   * This array maps encoding length to header bits in the first two bytes for SignedNumIncreasing

   * encoding.

   */

  private static final byte[][] LENGTH_TO_HEADER_BITS = {

    {0, 0},

    {(byte) 0x80, 0},

    {(byte) 0xc0, 0},

    {(byte) 0xe0, 0},

    {(byte) 0xf0, 0},

    {(byte) 0xf8, 0},

    {(byte) 0xfc, 0},

    {(byte) 0xfe, 0},

    {(byte) 0xff, 0},

    {(byte) 0xff, (byte) 0x80},

    {(byte) 0xff, (byte) 0xc0}

  };



  /**

   * This array maps encoding lengths to the header bits that overlap with the payload and need

   * fixing during readSignedNumIncreasing.

   */

  private static final long[] LENGTH_TO_MASK = {

    0L,

    0x80L,

    0xc000L,

    0xe00000L,

    0xf0000000L,

    0xf800000000L,

    0xfc0000000000L,

    0xfe000000000000L,

    0xff00000000000000L,

    0x8000000000000000L,

    0L

  };



  /**

   * This array maps the number of bits in a number to the encoding length produced by

   * WriteSignedNumIncreasing. For positive numbers, the number of bits is 1 plus the most

   * significant bit position (the highest bit position in a positive long is 63). For a negative

   * number n, we count the bits in ~n. That is, length = BITS_TO_LENGTH[log2Floor(n < 0 ? ~n : n) +

   * 1].

   */

  private static final short[] BITS_TO_LENGTH = {

    1, 1, 1, 1, 1, 1, 1,

    2, 2, 2, 2, 2, 2, 2,

    3, 3, 3, 3, 3, 3, 3,

    4, 4, 4, 4, 4, 4, 4,

    5, 5, 5, 5, 5, 5, 5,

    6, 6, 6, 6, 6, 6, 6,

    7, 7, 7, 7, 7, 7, 7,

    8, 8, 8, 8, 8, 8, 8,

    9, 9, 9, 9, 9, 9, 9,

    10

  };



  // stores the current encoded value as a list of byte arrays. Note that this

  // is manipulated as we read/write items.

  // Note that every item will fit on at most one array. One array may

  // have more than one item (eg when used for decoding). While encoding,

  // one array will have exactly one item. While returning the encoded array

  // we will merge all the arrays in this list.

  private final ArrayList<byte[]> encodedArrays = new ArrayList<>();



  // This is the current position on the first array. Will be non-zero

  // only if the ordered code was created using encoded byte array.

  private int firstArrayPosition = 0;



  /** Creates OrderedCode from scratch. Typically used at encoding time. */

  public OrderedCode() {}



  /**

   * Creates OrderedCode from a given encoded byte array. Typically used at decoding time.

   *

   * <p><b> For better performance, it uses the input array provided (not a copy). Therefore the

   * input array should not be modified.</b>

   */

  public OrderedCode(byte[] encodedByteArray) {

    encodedArrays.add(encodedByteArray);

  }



  /**

   * Adds the given byte array item to the OrderedCode. It encodes the input byte array, followed by

   * a separator and appends the result to its internal encoded byte array store.

   *

   * <p>It works with the input array, so the input array 'value' should not be modified till the

   * method returns.

   *

   * @param value bytes to be written.

   * @see #readBytes()

   */

  public void writeBytes(byte[] value) {

    writeBytes(value, false);

  }



  public void writeBytesDecreasing(byte[] value) {

    writeBytes(value, true);

  }



  private void writeBytes(byte[] value, boolean invert) {

    // Determine the length of the encoded array

    int encodedLength = 2; // for separator

    for (byte b : value) {

      if ((b == ESCAPE1) || (b == ESCAPE2)) {

        encodedLength += 2;

      } else {

        encodedLength++;

      }

    }



    byte[] encodedArray = new byte[encodedLength];

    int copyStart = 0;

    int outIndex = 0;

    for (int i = 0; i < value.length; i++) {

      byte b = value[i];

      if (b == ESCAPE1) {

        arraycopy(invert, value, copyStart, encodedArray, outIndex, i - copyStart);

        outIndex += i - copyStart;

        encodedArray[outIndex++] = convert(invert, ESCAPE1);

        encodedArray[outIndex++] = convert(invert, NULL_CHARACTER);

        copyStart = i + 1;

      } else if (b == ESCAPE2) {

        arraycopy(invert, value, copyStart, encodedArray, outIndex, i - copyStart);

        outIndex += i - copyStart;

        encodedArray[outIndex++] = convert(invert, ESCAPE2);

        encodedArray[outIndex++] = convert(invert, FF_CHARACTER);

        copyStart = i + 1;

      }

    }

    if (copyStart < value.length) {

      arraycopy(invert, value, copyStart, encodedArray, outIndex, value.length - copyStart);

      outIndex += value.length - copyStart;

    }

    encodedArray[outIndex++] = convert(invert, ESCAPE1);

    encodedArray[outIndex] = convert(invert, SEPARATOR);



    encodedArrays.add(encodedArray);

  }



  private static byte convert(boolean invert, byte val) {

    return invert ? (byte) ~val : val;

  }



  private static byte invert(byte val) {

    return convert(true, val);

  }



  private void arraycopy(

      boolean invert, byte[] src, int srcPos, byte[] dest, int destPos, int length) {

    System.arraycopy(src, srcPos, dest, destPos, length);

    if (invert) {

      for (int i = destPos; i < destPos + length; i++) {

        dest[i] = (byte) ~dest[i];

      }

    }

  }



  /**

   * Encodes the long item, in big-endian format, and appends the result to its internal encoded

   * byte array store.

   *

   * @see #readNumIncreasing()

   */

  public void writeNumIncreasing(long value) {

    // Values are encoded with a single byte length prefix, followed

    // by the actual value in big-endian format with leading 0 bytes

    // dropped.

    byte[] bufer = new byte[9]; // 8 bytes for value plus one byte for length

    int len = 0;

    while (value != 0) {

      len++;

      bufer[9 - len] = (byte) (value & 0xff);

      value >>>= 8;

    }

    bufer[9 - len - 1] = (byte) len;

    len++;

    byte[] encodedArray = new byte[len];

    System.arraycopy(bufer, 9 - len, encodedArray, 0, len);

    encodedArrays.add(encodedArray);

  }



  public void writeNumIncreasing(UnsignedInteger unsignedInt) {

    writeNumIncreasing(unsignedInt.longValue());

  }



  /**

   * Encodes the long item, in big-endian format, and appends the result to its internal encoded

   * byte array store.

   *

   * @see #readNumIncreasing()

   */

  public void writeNumDecreasing(long value) {

    // Values are encoded with a complemented single byte length prefix,

    // followed by the complement of the actual value in big-endian format with

    // leading 0xff bytes dropped.

    byte[] bufer = new byte[9]; // 8 bytes for value plus one byte for length

    int len = 0;

    while (value != 0) {

      len++;

      bufer[9 - len] = (byte) ~(value & 0xff);

      value >>>= 8;

    }

    bufer[9 - len - 1] = (byte) ~len;

    len++;

    byte[] encodedArray = new byte[len];

    System.arraycopy(bufer, 9 - len, encodedArray, 0, len);

    encodedArrays.add(encodedArray);

  }



  public void writeNumDecreasing(UnsignedInteger unsignedInt) {

    writeNumDecreasing(unsignedInt.longValue());

  }



  /** Return floor(log2(n)) for positive integer n. Returns -1 iff n == 0. */

  @VisibleForTesting

  int log2Floor(long n) {

    checkArgument(n >= 0);

    return n == 0 ? -1 : LongMath.log2(n, RoundingMode.FLOOR);

  }



  /** Calculates the encoding length in bytes of the signed number n. */

  @VisibleForTesting

  int getSignedEncodingLength(long n) {

    return BITS_TO_LENGTH[log2Floor(n < 0 ? ~n : n) + 1];

  }



  /** @see #readSignedNumIncreasing() */

  public void writeSignedNumIncreasing(long val) {

    long x = val < 0 ? ~val : val;

    if (x < 64) { // Fast path for encoding length == 1.

      byte[] encodedArray = new byte[] {(byte) (LENGTH_TO_HEADER_BITS[1][0] ^ val)};

      encodedArrays.add(encodedArray);

      return;

    }

    // buf = val in network byte order, sign extended to 10 bytes.

    byte signByte = val < 0 ? (byte) 0xff : 0;

    byte[] buf = new byte[2 + Longs.BYTES];

    buf[0] = buf[1] = signByte;

    System.arraycopy(Longs.toByteArray(val), 0, buf, 2, Longs.BYTES);

    int len = getSignedEncodingLength(x);

    if (len < 2) {

      throw new IllegalStateException(

          "Invalid length (" + len + ")" + " returned by getSignedEncodingLength(" + x + ")");

    }

    int beginIndex = buf.length - len;

    buf[beginIndex] ^= LENGTH_TO_HEADER_BITS[len][0];

    buf[beginIndex + 1] ^= LENGTH_TO_HEADER_BITS[len][1];



    byte[] encodedArray = new byte[len];

    System.arraycopy(buf, beginIndex, encodedArray, 0, len);

    encodedArrays.add(encodedArray);

  }



  public void writeSignedNumDecreasing(long val) {

    writeSignedNumIncreasing(~val);

  }



  /**

   * Encodes and appends INFINITY item to its internal encoded byte array store.

   *

   * @see #readInfinity()

   */

  public void writeInfinity() {

    writeTrailingBytes(INFINITY_ENCODED);

  }



  /**

   * Encodes and appends INFINITY item which would come before any real string.

   *

   * @see #readInfinityDecreasing()

   */

  public void writeInfinityDecreasing() {

    writeTrailingBytes(INFINITY_ENCODED_DECREASING);

  }



  /**

   * Appends the byte array item to its internal encoded byte array store. This is used for the last

   * item and is not encoded.

   *

   * <p>It stores the input array in the store, so the input array 'value' should not be modified.

   *

   * @param value bytes to be written.

   * @see #readTrailingBytes()

   */

  public void writeTrailingBytes(byte[] value) {

    if ((value == null) || (value.length == 0)) {

      throw new IllegalArgumentException("Value cannot be null or have 0 elements");

    }



    encodedArrays.add(value);

  }



  /**

   * Returns the next byte array item from its encoded byte array store and removes the item from

   * the store.

   *

   * @see #writeBytes(byte[])

   */

  public byte[] readBytes() {

    return readBytes(false);

  }



  public byte[] readBytesDecreasing() {

    return readBytes(true);

  }



  private byte[] readBytes(boolean invert) {

    if (encodedArrays.isEmpty() || ((encodedArrays.get(0)).length - firstArrayPosition <= 0)) {

      throw new IllegalArgumentException("Invalid encoded byte array");

    }



    // Determine the length of the decoded array

    // We only scan up to "length-2" since a valid string must end with

    // a two character terminator: 'ESCAPE1 SEPARATOR'

    byte[] store = encodedArrays.get(0);

    int decodedLength = 0;

    boolean valid = false;

    int i = firstArrayPosition;

    while (i < store.length - 1) {

      byte b = store[i++];

      if (b == convert(invert, ESCAPE1)) {

        b = store[i++];

        if (b == convert(invert, SEPARATOR)) {

          valid = true;

          break;

        } else if (b == convert(invert, NULL_CHARACTER)) {

          decodedLength++;

        } else {

          throw new IllegalArgumentException("Invalid encoded byte array");

        }

      } else if (b == convert(invert, ESCAPE2)) {

        b = store[i++];

        if (b == convert(invert, FF_CHARACTER)) {

          decodedLength++;

        } else {

          throw new IllegalArgumentException("Invalid encoded byte array");

        }

      } else {

        decodedLength++;

      }

    }

    if (!valid) {

      throw new IllegalArgumentException("Invalid encoded byte array");

    }



    byte[] decodedArray = new byte[decodedLength];

    int copyStart = firstArrayPosition;

    int outIndex = 0;

    int j = firstArrayPosition;

    while (j < store.length - 1) {

      byte b = store[j++]; // note that j has been incremented

      if (b == convert(invert, ESCAPE1)) {

        arraycopy(invert, store, copyStart, decodedArray, outIndex, j - copyStart - 1);

        outIndex += j - copyStart - 1;

        // ESCAPE1 SEPARATOR ends component

        // ESCAPE1 NULL_CHARACTER represents '\0'

        b = store[j++];

        if (b == convert(invert, SEPARATOR)) {

          if ((store.length - j) == 0) {

            // we are done with the first array

            encodedArrays.remove(0);

            firstArrayPosition = 0;

          } else {

            firstArrayPosition = j;

          }

          return decodedArray;

        } else if (b == convert(invert, NULL_CHARACTER)) {

          decodedArray[outIndex++] = 0x00;

        } // else not required - handled during length determination

        copyStart = j;

      } else if (b == convert(invert, ESCAPE2)) {

        arraycopy(invert, store, copyStart, decodedArray, outIndex, j - copyStart - 1);

        outIndex += j - copyStart - 1;

        // ESCAPE2 FF_CHARACTER represents '\xff'

        // ESCAPE2 INFINITY is an error

        b = store[j++];

        if (b == convert(invert, FF_CHARACTER)) {

          decodedArray[outIndex++] = (byte) 0xff;

        } // else not required - handled during length determination

        copyStart = j;

      }

    }

    // not required due to the first phase, but need to entertain the compiler

    throw new IllegalArgumentException("Invalid encoded byte array");

  }



  /**

   * Returns the next long item (encoded in big-endian format via {@code writeNumIncreasing(long)})

   * from its internal encoded byte array store and removes the item from the store.

   *

   * @see #writeNumIncreasing(long)

   */

  public long readNumIncreasing() {

    if (encodedArrays.isEmpty() || ((encodedArrays.get(0)).length - firstArrayPosition < 1)) {

      throw new IllegalArgumentException("Invalid encoded byte array");

    }



    byte[] store = encodedArrays.get(0);

    // Decode length byte

    int len = store[firstArrayPosition];

    if ((firstArrayPosition + len + 1 > store.length) || len > 8) {

      throw new IllegalArgumentException("Invalid encoded byte array");

    }



    long result = 0;

    for (int i = 0; i < len; i++) {

      result <<= 8;

      result |= (store[firstArrayPosition + i + 1] & 0xff);

    }



    if ((store.length - firstArrayPosition - len - 1) == 0) {

      // we are done with the first array

      encodedArrays.remove(0);

      firstArrayPosition = 0;

    } else {

      firstArrayPosition = firstArrayPosition + len + 1;

    }



    return result;

  }



  /**

   * Returns the next long item (encoded in big-endian format via {@code writeNumDecreasing(long)})

   * from its internal encoded byte array store and removes the item from the store.

   *

   * @see #writeNumDecreasing(long)

   */

  public long readNumDecreasing() {

    if (encodedArrays.isEmpty() || ((encodedArrays.get(0)).length - firstArrayPosition < 1)) {

      throw new IllegalArgumentException("Invalid encoded byte array");

    }



    byte[] store = encodedArrays.get(0);

    // Decode length byte

    int len = ~store[firstArrayPosition] & 0xff;

    if ((firstArrayPosition + len + 1 > store.length) || len > 8) {

      throw new IllegalArgumentException("Invalid encoded byte array");

    }



    long result = 0;

    for (int i = 0; i < len; i++) {

      result <<= 8;

      result |= (~store[firstArrayPosition + i + 1] & 0xff);

    }



    if ((store.length - firstArrayPosition - len - 1) == 0) {

      // we are done with the first array

      encodedArrays.remove(0);

      firstArrayPosition = 0;

    } else {

      firstArrayPosition = firstArrayPosition + len + 1;

    }



    return result;

  }



  /**

   * Returns the next long item (encoded via {@code writeSignedNumIncreasing(long)}) from its

   * internal encoded byte array store and removes the item from the store.

   *

   * @see #writeSignedNumIncreasing(long)

   */

  public long readSignedNumIncreasing() {

    if (encodedArrays.isEmpty() || ((encodedArrays.get(0)).length - firstArrayPosition < 1)) {

      throw new IllegalArgumentException("Invalid encoded byte array");

    }



    byte[] store = encodedArrays.get(0);



    long xorMask = ((store[firstArrayPosition] & 0x80) == 0) ? ~0L : 0L;

    // Store first byte as an int rather than a (signed) byte -- to avoid

    // accidental byte-to-int promotion later which would extend the byte's

    // sign bit (if any).

    int firstByte = (store[firstArrayPosition] & 0xff) ^ (int) (xorMask & 0xff);



    // Now calculate and test length, and set x to raw (unmasked) result.

    int len;

    long x;

    if (firstByte != 0xff) {

      len = 7 - log2Floor(firstByte ^ 0xff);

      if (store.length - firstArrayPosition < len) {

        throw new IllegalArgumentException("Invalid encoded byte array");

      }

      x = xorMask; // Sign extend using xorMask.

      for (int i = firstArrayPosition; i < firstArrayPosition + len; i++) {

        x = (x << 8) | (store[i] & 0xff);

      }

    } else {

      len = 8;

      if (store.length - firstArrayPosition < len) {

        throw new IllegalArgumentException("Invalid encoded byte array");

      }

      int secondByte = (store[firstArrayPosition + 1] & 0xff) ^ (int) (xorMask & 0xff);

      if (secondByte >= 0x80) {

        if (secondByte < 0xc0) {

          len = 9;

        } else {

          int thirdByte = (store[firstArrayPosition + 2] & 0xff) ^ (int) (xorMask & 0xff);

          if (secondByte == 0xc0 && thirdByte < 0x80) {

            len = 10;

          } else {

            // Either len > 10 or len == 10 and #bits > 63.

            throw new IllegalArgumentException("Invalid encoded byte array");

          }

        }

        if (store.length - firstArrayPosition < len) {

          throw new IllegalArgumentException("Invalid encoded byte array");

        }

      }

      x =

          Longs.fromByteArray(

              Arrays.copyOfRange(store, firstArrayPosition + len - 8, firstArrayPosition + len));

    }



    x ^= LENGTH_TO_MASK[len]; // Remove spurious header bits.



    if (len != getSignedEncodingLength(x)) {

      throw new IllegalArgumentException("Invalid encoded byte array");

    }



    if ((store.length - firstArrayPosition - len) == 0) {

      // We are done with the first array.

      encodedArrays.remove(0);

      firstArrayPosition = 0;

    } else {

      firstArrayPosition = firstArrayPosition + len;

    }



    return x;

  }



  public long readSignedNumDecreasing() {

    return ~readSignedNumIncreasing();

  }



  /**

   * Removes INFINITY item from its internal encoded byte array store if present. Returns whether

   * INFINITY was present.

   *

   * @see #writeInfinity()

   */

  public boolean readInfinity() {

    return readInfinityInternal(INFINITY_ENCODED);

  }



  /**

   * Removes INFINITY item from its internal encoded byte array store if present. Returns whether

   * INFINITY was present.

   *

   * @see #writeInfinityDecreasing()

   */

  public boolean readInfinityDecreasing() {

    return readInfinityInternal(INFINITY_ENCODED_DECREASING);

  }



  private boolean readInfinityInternal(byte[] codes) {

    if (encodedArrays.isEmpty() || ((encodedArrays.get(0)).length - firstArrayPosition < 1)) {

      throw new IllegalArgumentException("Invalid encoded byte array");

    }

    byte[] store = encodedArrays.get(0);

    if (store.length - firstArrayPosition < 2) {

      return false;

    }

    if ((store[firstArrayPosition] == codes[0]) && (store[firstArrayPosition + 1] == codes[1])) {

      if ((store.length - firstArrayPosition - 2) == 0) {

        // we are done with the first array

        encodedArrays.remove(0);

        firstArrayPosition = 0;

      } else {

        firstArrayPosition = firstArrayPosition + 2;

      }

      return true;

    } else {

      return false;

    }

  }



  /**

   * Returns the trailing byte array item from its internal encoded byte array store and removes the

   * item from the store.

   *

   * @see #writeTrailingBytes(byte[])

   */

  public byte[] readTrailingBytes() {

    // one item is contained within one byte array

    if (encodedArrays.size() != 1) {

      throw new IllegalArgumentException("Invalid encoded byte array");

    }



    byte[] store = encodedArrays.get(0);

    encodedArrays.remove(0);

    assert encodedArrays.isEmpty();

    return Arrays.copyOfRange(store, firstArrayPosition, store.length);

  }



  /**

   * Returns the encoded bytes that represents the current state of the OrderedCode.

   *

   * <p><b> NOTE: This method returns OrederedCode's internal array (not a copy) for better

   * performance. Therefore the returned array should not be modified.</b>

   */

  public byte[] getEncodedBytes() {

    if (encodedArrays.isEmpty()) {

      return new byte[0];

    }

    if ((encodedArrays.size() == 1) && (firstArrayPosition == 0)) {

      return encodedArrays.get(0);

    }



    int totalLength = 0;



    for (int i = 0; i < encodedArrays.size(); i++) {

      byte[] bytes = encodedArrays.get(i);

      if (i == 0) {

        totalLength += bytes.length - firstArrayPosition;

      } else {

        totalLength += bytes.length;

      }

    }



    byte[] encodedBytes = new byte[totalLength];

    int destPos = 0;

    for (int i = 0; i < encodedArrays.size(); i++) {

      byte[] bytes = encodedArrays.get(i);

      if (i == 0) {

        System.arraycopy(

            bytes, firstArrayPosition, encodedBytes, destPos, bytes.length - firstArrayPosition);

        destPos += bytes.length - firstArrayPosition;

      } else {

        System.arraycopy(bytes, 0, encodedBytes, destPos, bytes.length);

        destPos += bytes.length;

      }

    }



    // replace the store with merged array, so that repeated calls

    // don't need to merge. The reads can handle both the versions.

    encodedArrays.clear();

    encodedArrays.add(encodedBytes);

    firstArrayPosition = 0;



    return encodedBytes;

  }



  /**

   * Returns true if it has more encoded bytes that haven't been read, false otherwise. Return value

   * of true doesn't imply anything about validity of remaining data.

   *

   * @return true if it has more encoded bytes that haven't been read, false otherwise.

   */

  public boolean hasRemainingEncodedBytes() {

    // We delete an array after fully consuming it.

    return encodedArrays.size() != 0;

  }

}